package totp;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import totp.Base32String.DecodingException;


/**
 * This class implements the TOTP(Time-Based One-Time Password) algorithm
 * described in RFC 6238.
 * 
 * @author Ivan,Zhang
 * @see <a href="https://tools.ietf.org/html/rfc6238" />
 *
 */
public class TOTPAuthenticator {
	private static final int powerOfTOTPLength = (int) Math.pow(10, TOTPConfig.TOTPLength);

	/**
	 * generate a time-based one-time password with a given key at the receiving
	 * time.
	 * 
	 * @param key
	 *            the secret key in binary format.
	 * @return one-time password generated by the secret key at the
	 */
	public String generateTOTP(byte[] key) {
		return calculateTOTP(key, getTimeStepWindowFromTime(new Date().getTime()));
	}

	/**
	 * check the validity of the given secret key and the time-based one-time
	 * password.The validation system should compare OTPs not only with the
	 * receiving timestamp but also the past time-steps that are within the
	 * allowedValidationWinows.
	 * 
	 * @see <a href="https://tools.ietf.org/html/rfc6238#section-5.2" />
	 * 
	 * @param key
	 *            the secret key in binary format.
	 * @param totpFromProver
	 *            password from prover to be validated.
	 * @return true if the totpFromProver equals any totp in the
	 *         allowedValidationWindows.
	 * 
	 */
	public boolean validateTOTP(byte[] key, String totpFromProver) {
		if (isTOTP(totpFromProver)) {
			final long receivedTimeStepWindow = getTimeStepWindowFromTime(new Date().getTime());
			for (int i = 0
					- TOTPConfig.allowedPastValidationWindows; i <= TOTPConfig.allowedFutureValidationWindows; i++) {
				String totpCalculated = calculateTOTP(key, receivedTimeStepWindow + i);
				if (totpCalculated.equals(totpFromProver)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * check the validity of the given secret key and the time-based one-time
	 * password.If the given secretKey contains characters that are not allowed
	 * in Base32String.base32Alphabet , this method will throw
	 * DecodingException.
	 * 
	 * @param secretKey
	 *            secret key in string format
	 * @param totpFromProver
	 *            password from prover to be validated.
	 * @return true if the totpFromProver equals any totp in the
	 *         allowedValidationWindows.
	 * @throws DecodingException
	 */
	public boolean validateTOTPUsingBase32Decoder(String secretKey, String totpFromProver) throws DecodingException {
		if(secretKey==null)
			return false;
		return validateTOTP(Base32String.decode(secretKey), totpFromProver);
	}

	/**
	 * Implementation of HOTP(HMAC-Based One-Time Password Algorithm),used to
	 * generate a password within a given hash algorithm and a timeStepWindow.
	 * if the keys are the same and time instants are in the same time-step
	 * window, the generated password is the same.
	 * </p>
	 * TOTP = HOTP(K,T).where T is an integer and represents the number of time
	 * steps between the initial counter time T0 and the current Unix time.
	 * </p>
	 * HOTP(K,C) = Truncate(HMAC-SHA-1(K,C)).see [RFC4226] for detailed
	 * definitions.
	 * </p>
	 * 
	 * @see	<a href="https://tools.ietf.org/html/rfc4226" />
	 * 
	 * @param key
	 *            the secret key in binary format.
	 * @param timeStepWindow
	 *            the time-step window of a time instance.
	 * @return one-time password generated by the secret key in the specific
	 *         time-step window.
	 */
	public String calculateTOTP(byte[] key, long timeStepWindow) {
		byte[] data = new byte[8];
		long value = timeStepWindow;
		for (int i = 8; i-- > 0; value >>>= 8) {
			data[i] = (byte) value;
		}
		SecretKeySpec signKey = new SecretKeySpec(key, TOTPConfig.secretKeyAlgorithm);
		try {
			//Step 1:Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C)
			Mac mac = Mac.getInstance(TOTPConfig.secretKeyAlgorithm);
			mac.init(signKey);
			byte[] hmac_result = mac.doFinal(data);
			// Step 2: Generate a 4-byte string,from byte hmac_result[offset] 
			int offset = hmac_result[hmac_result.length - 1] & 0xF;
			int truncatedHash = (hmac_result[offset]  & 0x7f) << 24
	           | (hmac_result[offset+1] & 0xff) << 16
	           | (hmac_result[offset+2] & 0xff) <<  8
	           | (hmac_result[offset+3] & 0xff) ;
			//Step 3:Compute an HOTP value
			truncatedHash %= powerOfTOTPLength;
			String totp = Integer.toString(truncatedHash);
			while (totp.length() < TOTPConfig.TOTPLength) {
				totp = "0" + totp;
			}
			return totp;
		} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
			ex.printStackTrace();
		}
		return "";
	}

	/**
	 * 
	 * @param time
	 *            instant in milliSeconds.
	 * @return the time-step window of a specific time instant.
	 */
	private long getTimeStepWindowFromTime(long milliSeconds) {
		return (milliSeconds - TOTPConfig.timeStart) / TOTPConfig.timeStepSize;
	}

	private boolean isTOTP(String str) {
		if (str != null) {
			if (str.length() == TOTPConfig.TOTPLength) {
				for (char c : str.toCharArray()) {
					if (!Character.isDigit(c)) {
						return false;
					}
				}
				return true;
			}
		}
		return false;
	}
}